{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5fb7798b-4c96-44e7-9822-6da31cd9ceba",
   "metadata": {},
   "source": [
    "# Color And Colormap Options\n",
    "\n",
    "Visual styling options to adjust colors and colormapping:\n",
    "\n",
    "```{eval-rst}\n",
    ".. plotting-options-table:: Color And Colormap Options\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1f05c88c-9ba7-4a2c-8f61-24ed4c0fd3cf",
   "metadata": {},
   "source": [
    "(option-bgcolor)=\n",
    "## `bgcolor`\n",
    "\n",
    "The `bgcolor` option sets the background color of the data area of the plot. It accepts any valid CSS color string such as 'white', 'lightgray', or hex codes like '#f0f0f0'. This can be useful to improve contrast or match the theme of a larger dashboard or presentation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f05fd7d-bf2d-472b-b049-85ade434c653",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.pandas  # noqa\n",
    "\n",
    "df = hvplot.sampledata.earthquakes(\"pandas\")\n",
    "\n",
    "df.hvplot.scatter(\n",
    "    x='lon', y='lat', c='mag', cmap='inferno_r',\n",
    "    bgcolor='#f5f5f5',  # light gray background\n",
    "    title=\"Earthquake Magnitudes by Location\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ebc3e58d-2f6e-4ea5-8958-f21446e9a8a6",
   "metadata": {},
   "source": [
    "(option-color)=\n",
    "## `color`\n",
    "\n",
    "The `color` option sets the color of the plotted elements. It accepts:\n",
    "\n",
    "- A single color name or hex code (e.g., `'red'`, `'#1f77b4'`) to apply uniformly.\n",
    "- A column name to map colors based on data values (for categorical or continuous color encoding).\n",
    "- A list of colors to cycle through when plotting multiple groups (e.g., with `by='column'`).\n",
    "- A column containing per-point color values (e.g., hex codes).\n",
    "\n",
    "If both `color` and `c` are provided, `color` takes precedence. For categorical data, hvPlot automatically uses a discrete colormap unless overridden."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb51c665-3b3a-4425-b71a-0723e19cb35f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.pandas  # noqa\n",
    "\n",
    "df = hvplot.sampledata.penguins(\"pandas\")\n",
    "\n",
    "plot_opts = dict(x=\"flipper_length_mm\", y=\"body_mass_g\", frame_width=250)\n",
    "(\n",
    "    df.hvplot.scatter(\n",
    "        color=\"#d133ff\", title=\"Colored by single hex color\", **plot_opts\n",
    "    ) +\n",
    "    df.hvplot.scatter(\n",
    "        color=\"species\", title=\"Colored by column name\", **plot_opts\n",
    "    ) +\n",
    "    df.hvplot.scatter(\n",
    "        by=\"species\", color=[\"blue\", \"green\", \"orange\"],\n",
    "        title=\"Groups colored by custom list\", **plot_opts\n",
    "    ) +\n",
    "    df.hvplot.scatter(\n",
    "        color=df['species'].map({'Adelie': 'red', 'Gentoo': 'purple', 'Chinstrap': 'grey'}),\n",
    "        title=\"Per-point color value\", **plot_opts,\n",
    "    )\n",
    ").cols(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5706683c-c0c1-42bb-b390-cb65ff5eba5a",
   "metadata": {},
   "source": [
    "::: {note}\n",
    "\n",
    "**Coloring Categorical Data: `color` vs `by`**\n",
    "\n",
    "When visualizing categorical data, both the `color` and `by` keywords can produce color-separated groups, but they behave differently:\n",
    "\n",
    "- **`color='<categorical column name>'`** performs vectorized color mapping within a single plot. Each unique value in the column is mapped to a distinct color, using the default `glasbey_category10` categorical colormap. This approach is efficient and results in a single-layer plot with all points colored by their category.\n",
    "\n",
    "- **`by='<categorical column name>'`** splits the data into multiple groups and creates an [overlay](inv:holoviews#holoviews.core.element.NdOverlay) of plots, one per unique value in the column. However, it does **not** honor the `cmap` you provide. Instead, it assigns colors by cycling through a color list.\n",
    "If no colors are specified, it defaults to the `glasbey_hv` color cycle. You can override this by passing a custom list of colors to `color=[...]`.\n",
    "\n",
    "\n",
    "In addition, using `color='<categorical column>'` is usually faster than `by='<categorical column>'`, especially when working with large datasets or many unique categories. This is because `color` performs vectorized color mapping within a single plot, while `by` generates an overlay of multiple plots — each of which is rendered and managed separately. See [`by` keyword](option-by)\n",
    "\n",
    "**Summary of differences:**\n",
    "\n",
    "| Keyword | Coloring Mechanism  | Supports `cmap` | Default Color Scheme  | Faster\n",
    "|---------|---------------------|-----------------|-----------------------| ---\n",
    "| `color` | Vectorized color mapping | ✅ Yes | `glasbey_category10` | ✅Yes\n",
    "| `by`    | Splits into overlays by category | ❌ No | `glasbey_hv` | ❌ No\n",
    "\n",
    "Use `color` when you want a single plot with color-encoded values.  \n",
    "Use `by` when you want separate layers per category — for subplots or layout purposes.\n",
    ":::\n",
    "\n",
    ":::{seealso}\n",
    "[`cmap`](option-cmap)\n",
    ":::"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "be669a46",
   "metadata": {},
   "source": [
    "::: {tip} Categorical Custom Color Mapping: Best Practice\n",
    "\n",
    "When you want to map categorical values to specific colors, use `color='column_name'` with `cmap={...}` instead of passing a pre-mapped Series:\n",
    "\n",
    "**✅ Recommended:**\n",
    "```python\n",
    "df.hvplot.scatter(\n",
    "    x='x', y='y', color='species',\n",
    "    cmap={'Adelie': 'blue', 'Gentoo': 'yellow', 'Chinstrap': 'red'}\n",
    ")\n",
    "```\n",
    "\n",
    "**❌ Not recommended:**\n",
    "```python\n",
    "df.hvplot.scatter(\n",
    "    x='x', y='y',\n",
    "    color=df['species'].map({'Adelie': 'blue', ...})\n",
    ")\n",
    "```\n",
    "\n",
    "Using `color='column_name'` with `cmap` keeps your original data visible in hover tooltips and provides a cleaner, more maintainable API.\n",
    ":::"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a03fc23-d338-44df-9777-9f06ad1096eb",
   "metadata": {},
   "source": [
    "(option-c)=\n",
    "## `c`\n",
    "\n",
    "Alias for [`color`](option-color) above."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3a1bc8fd-4667-483c-996a-d015da754cff",
   "metadata": {},
   "source": [
    "(option-cmap)=\n",
    "## `cmap`\n",
    "\n",
    "The `cmap` option controls the colormap used when mapping numerical or categorical data values to color. It supports:\n",
    "- Named colormaps from [Bokeh](https://docs.bokeh.org/en/latest/docs/reference/palettes.html#bokeh-palettes), [Matplotlib](https://matplotlib.org/stable/users/explain/colors/colormaps.html#choosing-colormaps-in-matplotlib), or [Colorcet](https://colorcet.holoviz.org/user_guide/index.html#accessing-the-colormaps) (e.g., `'viridis'`, `'plasma'`, `'coolwarm'`)\n",
    "- Lists of color strings or hex codes (for custom sequences)\n",
    "- Dictionaries (for categorical color mappings)\n",
    "- Colormap objects from Matplotlib or [HoloViews](https://holoviews.org/user_guide/Colormaps.html#available-colormaps)\n",
    "\n",
    "hvPlot selects a default colormap based on the data type, but `cmap` lets you override this behavior. Only one of `cmap`, `colormap`, or `color_key` should be used at a time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3c66bedd-6ec9-40ce-8c6b-2c5cf9526c5c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.pandas  # noqa\n",
    "import matplotlib as mpl\n",
    "\n",
    "df = hvplot.sampledata.earthquakes(\"pandas\")\n",
    "\n",
    "plot_opts = dict(x=\"lon\", y=\"lat\", frame_width=250)\n",
    "plot1 = df.hvplot.scatter(\n",
    "    cmap=\"plasma_r\", title=\"Named cmap 'plasma_r'\", color=\"mag\", **plot_opts\n",
    ")\n",
    "plot2 = df.hvplot.scatter(\n",
    "    cmap=[\n",
    "        '#f7fbff', '#deebf7', '#c6dbef', '#9ecae1',\n",
    "        '#6baed6', '#4292c6', '#2171b5', '#084594',\n",
    "    ], title=\"Custom list cmap\", color=\"mag\", **plot_opts\n",
    ")\n",
    "plot3 = df.hvplot.scatter(\n",
    "    cmap=mpl.colormaps[\"OrRd\"], title=\"MPL colormap object 'OrRd'\", color=\"mag\", **plot_opts\n",
    ")\n",
    "plot4 = df.hvplot.scatter(\n",
    "    cmap={\n",
    "        'Shallow': 'orange',\n",
    "        'Intermediate': '#C70039',\n",
    "        'Deep': '#581845',\n",
    "    }, title=\"Dict keys categorical cmap\", color=\"depth_class\", **plot_opts,\n",
    ")\n",
    "(plot1 + plot2 + plot3 + plot4).cols(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "861a158b-be6a-46ad-a556-a8cc8d9c069d",
   "metadata": {},
   "source": [
    "::: {tip}\n",
    "**Prefer `Colorcet` for color mapping**\n",
    "\n",
    "While you can use any valid Matplotlib or Bokeh colormap with `cmap`, we recommend using the [`Colorcet`](https://colorcet.holoviz.org) library when possible. It provides a wide selection of perceptually accurate, well-tested colormaps optimized for data visualization.\n",
    "\n",
    "`Colorcet` is also natively integrated with other HoloViz tools like [HoloViews](https://holoviews.org) and [Datashader](https://datashader.org), which means:\n",
    "- No extra setup is required\n",
    "- Colormaps work consistently across all visualizations\n",
    "- Defaults like `glasbey_category10` and `kbc_r` come from `Colorcet`\n",
    "\n",
    "This makes it a reliable and visually accessible choice for both scientific and presentation-quality plots.\n",
    ":::"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c34a309-d7c0-423e-adc7-9743f3d7a331",
   "metadata": {},
   "source": [
    "(option-colormap)=\n",
    "## `colormap`\n",
    "\n",
    "Alias for [`cmap`](option-cmap) above."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3290b419-10a3-4d2b-ac99-0e50c644fbcc",
   "metadata": {},
   "source": [
    "(option-color_key)=\n",
    "## `color_key`\n",
    "\n",
    "Alias for [`cmap`](option-cmap) above."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b27eb1cc-3a6e-400b-816d-094a22bffac9",
   "metadata": {},
   "source": [
    "(option-clim)=\n",
    "## `clim`\n",
    "\n",
    "The `clim` option sets the lower and upper bounds of the color scale for continuous color mapping. It accepts a tuple like `(min, max)` and is useful when:\n",
    "\n",
    "- You want consistent color scaling across multiple plots.\n",
    "- You want to clip outliers or focus on a specific data range.\n",
    "\n",
    "This option is most effective when used with **gridded plots** like `image`, `heatmap`, or rasterized plots (`datashade=True`, `rasterize=True`). It works especially well with **xarray datasets** or plots where a **colorbar** is enabled.\n",
    "\n",
    "In standard scatter plots or overlays created from tabular (pandas) data, `clim` may have no visible effect unless color mapping and a colorbar are explicitly involved.\n",
    "\n",
    "If `clim` is not specified, the color scale is inferred from the data — or from the 2nd and 98th percentiles when `robust=True`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "74d74cec-a37a-4be3-b8e2-9a607e20590d",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.xarray  # noqa\n",
    "\n",
    "df = hvplot.sampledata.air_temperature(\"xarray\").sel(time=\"2014-02-25 12:00\")\n",
    "\n",
    "plot1 = df.hvplot.image(clim=(250, 280), title=\"Colorbar clipped at clim values\", frame_width=250)\n",
    "plot2 = df.hvplot.image(title=\"Default colorbar scale\", frame_width=250)\n",
    "plot1 + plot2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c2622459-8eba-49bb-bb3f-468ca6231483",
   "metadata": {},
   "source": [
    "::: {seealso}\n",
    "[`robust`](option-robust).\n",
    ":::"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35b5e132-8fde-4637-8289-e29bc0bb4cb5",
   "metadata": {},
   "source": [
    "(option-cnorm)=\n",
    "## `cnorm`\n",
    "\n",
    "The `cnorm` option controls how data values are mapped to colors in a colormap. It affects the distribution of colors across the range of values.\n",
    "\n",
    "Accepted values:\n",
    "- 'linear' (default): evenly maps values across the colormap.\n",
    "- 'log': applies logarithmic scaling, useful for data with large dynamic range.\n",
    "- 'eq_hist': uses [histogram equalization](https://datashader.org/user_guide/Plotting_Pitfalls.html) to emphasize contrast in sparse or skewed data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18379de6-42dd-4e1d-9a6c-740c6fe226a7",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.pandas  # noqa\n",
    "\n",
    "df = hvplot.sampledata.earthquakes(\"pandas\")\n",
    "\n",
    "opts = dict(\n",
    "    x='lon', y='lat', c='depth',\n",
    "    cmap='plasma_r', frame_width=200,\n",
    ")\n",
    "plot1 = df.hvplot.scatter(title=\"Linear (default) color scaling\", **opts,)\n",
    "plot2 = df.hvplot.scatter(cnorm='log', title=\"Log color scaling\", **opts,)\n",
    "plot3 = df.hvplot.scatter(cnorm='eq_hist', title=\"Histogram Equalization\", **opts,)\n",
    "(plot1 + plot2 + plot3).cols(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b7b0baa-bcc9-4195-b721-84dca8006cbe",
   "metadata": {},
   "source": [
    "(option-rescale_discrete_levels)=\n",
    "## `rescale_discrete_levels`\n",
    "\n",
    "The `rescale_discrete_levels` option improves the visual contrast of discrete values when using `cnorm='eq_hist'`. By default, it adjusts the lower bound of the colormap so that non-zero values appear higher on the scale–helpful when low counts would otherwise appear faded.\n",
    "\n",
    "This only has an effect if:\n",
    "- `cnorm='eq_hist'` is set\n",
    "- The color values are discrete (e.g., counts or categories)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8f08a504-dde4-435c-acf2-2e77449b402c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.pandas  # noqa\n",
    "\n",
    "df = hvplot.sampledata.earthquakes(\"pandas\")\n",
    "\n",
    "# Simulate discrete values by binning magnitude\n",
    "df['mag_bin'] = df['mag'].round()\n",
    "\n",
    "opts = dict(\n",
    "    x='lon', y='lat', c='mag_bin', cmap='viridis_r',\n",
    "    cnorm='eq_hist', frame_width=250,\n",
    ")\n",
    "plot1 = df.hvplot.scatter(title=\"Discrete Color Scaling (Default)\", **opts)\n",
    "plot2 = df.hvplot.scatter(rescale_discrete_levels=False, title=\"Without Discrete Color Scaling\", **opts)\n",
    "plot1 + plot2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "353f7840-ab51-4ba5-a89c-78415c43fca3",
   "metadata": {
    "tags": []
   },
   "source": [
    "(option-robust)=\n",
    "## `robust`\n",
    "\n",
    "The `robust` option adjusts how the colormap range is computed for image plots. When set to `True` and no explicit color limits (`clim`) are provided, hvPlot calculates the color limits based on the 2nd and 98th percentiles rather than the extreme minimum and maximum values, reducing the impact of outliers.\n",
    "\n",
    "In the following example using the `air_temperature` dataset, we will add 2 extreme data points to show how the `robust` keyword can help with visualizing the data without the outliers distorting the plot."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6c1a91ed-cf75-4ab9-8d90-ddb40892c686",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.xarray  # noqa\n",
    "\n",
    "ds = hvplot.sampledata.air_temperature(\"xarray\").sel(time=\"2014-02-25 12:00\")\n",
    "ds.air[0, 0] = 50\n",
    "ds.air[-1, -1] = 500\n",
    "\n",
    "opts = dict(width=350, cmap='viridis',)\n",
    "plot1 = ds.hvplot.image(title=\"Plot with outliers\", **opts)\n",
    "plot2 = ds.hvplot.image(robust=True, title=\"Using robust=True\", **opts)\n",
    "plot1 + plot2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9d9133b9-38f9-425f-a1fd-899f31f620ac",
   "metadata": {},
   "source": [
    "Notice how the colorbar in the second plot is now clipped to within the range of 240 to 290 with the colors in the plot evenly distributed."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2577d4dd-3822-4b86-980b-3692eb3f73dd",
   "metadata": {},
   "source": [
    "(option-symmetric)=\n",
    "## `symmetric`\n",
    "\n",
    "The `symmetric` option controls whether the colormap range is centered around zero. If you do not explicitly set `symmetric=True` and no color limits are provided via `clim`, hvPlot automatically checks your data by computing the 5th and 95th percentiles. If the 5th percentile is below 0 and the 95th percentile is above 0, the option is enabled so that the colormap is balanced about 0.\n",
    "\n",
    "::: {note}\n",
    "For lazily loaded or very large xarray datasets, this check is skipped for performance reasons and defaults to `False`.\n",
    ":::"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9b53e6d2-d877-46bb-ab6b-c7089148dab2",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.xarray  # noqa\n",
    "\n",
    "ds = hvplot.sampledata.air_temperature(\"xarray\")\n",
    "\n",
    "# Select a single date and convert to Celsius to get\n",
    "# both negative and positive values around 0\n",
    "data = ds.sel(time='2014-02-25 12:00') - 273.15\n",
    "plot1 = data.hvplot.image(title=\"Symmetric True by default\", width=350)\n",
    "plot2 = data.hvplot.image(symmetric=False, title=\"Symmetric=False\", width=350)\n",
    "plot1 + plot2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "636c5a77-94a7-4faa-ae9b-ffeb9c3b3bf8",
   "metadata": {},
   "source": [
    "In this example, the left image uses the symmetric colormap scaling (centered at zero), while the right image shows the default color scaling without enforcing symmetry. Notice that when the temperature values are symmetric around 0, the `coolwarm` colormap is used by default."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65477e9f-4a98-46ba-9734-26174da09a87",
   "metadata": {},
   "source": [
    "(option-check_symmetric_max)=\n",
    "## `check_symmetric_max`\n",
    "\n",
    "The `check_symmetric_max` option sets an upper limit on the number of data elements for which the automatic symmetry check is performed. When the dataset’s size exceeds this threshold, hvPlot skips the symmetry check and defaults to treating the data as non-symmetric. By default this limit is **1,000,000** elements which usually works well for most datasets. However, you can adjust it if you want to force or avoid the symmetric check for smaller or larger datasets."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9be88ac-fbeb-4f1c-abca-270d1cc60347",
   "metadata": {},
   "outputs": [],
   "source": [
    "import hvplot.xarray  # noqa\n",
    "\n",
    "da = hvplot.sampledata.air_temperature(\"xarray\").sel(time=\"2014-02-25 12:00\") - 273.15\n",
    "\n",
    "plot1 = da.hvplot.image(width=350, title=\"Default check for symmetry\")\n",
    "plot2 = da.hvplot.image(check_symmetric_max=10, width=350, title=\"Avoid symmetry check above 10\")\n",
    "plot1 + plot2"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
